package gormc

import (
	"context"
	"errors"
	"path/filepath"
	"runtime"
	"strings"
	"time"

	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
	"gorm.io/gorm"
	glogger "gorm.io/gorm/logger"
)

var (
	gormPackage  = filepath.Join("gorm.io", "gorm")
	gormcPackage = filepath.Join("pkg", "gormc")
)

type ContextFn func(ctx context.Context) []zapcore.Field

type ZapLogger struct {
	*glogger.Config
	SkipCallerLookup bool
	ZapLogger        *zap.Logger
	Context          ContextFn
}

func NewZapLogger(zapLogger *zap.Logger) *ZapLogger {
	return &ZapLogger{
		ZapLogger: zapLogger,
		Config: &glogger.Config{
			SlowThreshold:             100 * time.Millisecond,
			IgnoreRecordNotFoundError: false,
			LogLevel:                  glogger.Warn,
		},
		SkipCallerLookup: false,
		Context:          nil,
	}
}

func (l *ZapLogger) LogMode(level glogger.LogLevel) glogger.Interface {
	l.Config.LogLevel = level
	return l
}

func (l *ZapLogger) Info(ctx context.Context, str string, args ...interface{}) {
	if l.LogLevel >= glogger.Info {
		l.logger(ctx).Sugar().Debugf(str, args...)
	}
}

func (l *ZapLogger) Warn(ctx context.Context, str string, args ...interface{}) {
	if l.LogLevel >= glogger.Warn {
		l.logger(ctx).Sugar().Warnf(str, args...)
	}
}

func (l *ZapLogger) Error(ctx context.Context, str string, args ...interface{}) {
	if l.LogLevel >= glogger.Error {
		l.logger(ctx).Sugar().Errorf(str, args...)
	}
}

func (l *ZapLogger) Trace(ctx context.Context, begin time.Time, fc func() (string, int64), err error) {
	if l.LogLevel <= glogger.Silent {
		return
	}
	elapsed := time.Since(begin)
	logger := l.logger(ctx)
	switch {
	case err != nil && l.LogLevel >= glogger.Error && (!errors.Is(err, gorm.ErrRecordNotFound) || !l.IgnoreRecordNotFoundError):
		sql, rows := fc()
		logger.Error(
			"traceE",
			zap.Error(err),
			zap.Duration("elapsed", elapsed),
			zap.Int64("rows", rows),
			zap.String("sql", sql),
		)
	case elapsed > l.SlowThreshold && l.SlowThreshold != 0 && l.LogLevel >= glogger.Warn:
		sql, rows := fc()
		logger.Warn("traceW", zap.Duration("elapsed", elapsed), zap.Int64("rows", rows), zap.String("sql", sql))
	case l.LogLevel == glogger.Info:
		sql, rows := fc()
		logger.Debug("traceI", zap.Duration("elapsed", elapsed), zap.Int64("rows", rows), zap.String("sql", sql))
	}
}

func (l *ZapLogger) logger(ctx context.Context) *zap.Logger {
	logger := l.ZapLogger
	if l.Context != nil {
		fields := l.Context(ctx)
		logger = logger.With(fields...)
	}

	if l.SkipCallerLookup {
		return logger
	}

	for i := 2; i < 15; i++ {
		_, file, _, ok := runtime.Caller(i)
		switch {
		case !ok:
		case strings.HasSuffix(file, "_test.go"):
		case strings.Contains(file, gormPackage):
		case strings.Contains(file, gormcPackage):
		default:
			return logger.WithOptions(zap.AddCallerSkip(i))
		}
	}
	return logger
}
